package com.xinyue.dao;

import com.alibaba.fastjson.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.StringUtils;

import java.time.Duration;
import java.util.Optional;

/**
 * 游戏数据对象持久化操作类，包括redis缓存和数据库存储。<br/>
 * 1. 如果是查询，会先查询redis中是否在存在数据，如果存在直接返回，如果不存在，再从数据库查询，如果数据库有数据，先存储到redis一份，再返回
 *    如果数据库之中也不存在，会在redis之中缓存一个默认标记，表示数据目前不存在。<br/>
 * 2. 如果是更新操作，先更新redis，再更新数据库。<br/>
 * 3. redis中的数据以json的格式存储<br/>
 * @param <Entity>
 * @param <ID>
 */
public class GameEntityRepositoryCacheDao<Entity extends GameEntity<ID>, ID> {
    //redis存储的默认值
    private static final String DefaultRedisNullValue = "#-#";
    protected Logger logger = LoggerFactory.getLogger(this.getClass());
    //redis操作类
    protected StringRedisTemplate redisTemplate;
    //redis key配置类
    protected IRedisKeyConfig redisKeyConfig;
    //数据库操作类
    protected MongoRepository<Entity, ID> mongoRepository;

    public GameEntityRepositoryCacheDao( StringRedisTemplate redisTemplate, IRedisKeyConfig redisKeyConfig, MongoRepository<Entity, ID> mongoRepository) {
        this.redisKeyConfig = redisKeyConfig;
        this.redisTemplate = redisTemplate;
        this.mongoRepository = mongoRepository;
    }

    protected String getRedisKey(ID entityId) {
        String param = entityId.toString();
        String redisKey = this.redisKeyConfig.buildKey(param);
        checkRedisKey(redisKey);
        return redisKey;
    }

    protected void checkRedisKey(String key) {
        if (StringUtils.isEmpty(key)) {
            throw new IllegalArgumentException("redis key 不能为空");
        }
        if (this.redisKeyConfig.getExpire() == null) {
            throw new IllegalArgumentException("必须为redis key设置过期时间");
        }
    }


    /**
     * <p>Description:先从redis缓存中获取，缓存中没有从数据库获取，获取成功之后会自动缓存到redis中 </p>
     *
     * @param entityId
     * @return
     * @author wang guang shuai
     * @date 2020年1月10日 下午6:46:00
     */
    public Optional<Entity> findById(ID entityId, Class<Entity> clazz) {
        Entity entity = loadValue(entityId, clazz);
        return Optional.ofNullable(entity);
    }

    private void updateRedis(Entity entity) {
        ID id  = entity.getId();
        if(id == null){
            throw new IllegalArgumentException("更新数据时，id不能为空");
        }
        String key = getRedisKey(id);
        String value = JSON.toJSONString(entity);
        Duration duration = this.redisKeyConfig.getExpire();
        redisTemplate.opsForValue().set(key, value, duration);
    }

    /**
     * <p>Description:将对象同时更新到redis和数据库中 </p>
     *
     * @param entity
     * @param id
     * @author wang guang shuai
     * @date 2020年1月10日 上午10:57:04
     */
    public void saveOrUpdate(Entity entity) {
        this.updateRedis(entity);
        mongoRepository.save(entity);
        logger.debug("更新成功,{}",entity);
    }

    /**
     * <p>
     * Description:第一次会从redis中获取，如果redis中没有此值，从db中获取
     * </p>
     *
     * @return
     * @author wang guang shuai
     * @date 2020年1月9日 下午8:07:52
     */
    private Entity loadValue(ID entityId, Class<Entity> clazz) {

        String redisKey = getRedisKey(entityId);
        Duration expire = this.redisKeyConfig.getExpire();
        String value = redisTemplate.opsForValue().get(redisKey);
        Entity entity = null;
        if (value == null) {
            // 加锁，防止缓存穿透和击穿
            synchronized (redisKey.intern()) {
                // 二次检测
                value = redisTemplate.opsForValue().get(redisKey);
                if (value == null) {// 如果等于空，从数据库取
                    entity = mongoRepository.findById(entityId).orElse(null);
                    if (entity == null) {// 如果数据库还是没有，说明是真的没有，添加空标记
                        value = DefaultRedisNullValue;
                    } else {
                        value = JSON.toJSONString(entity);
                    }
                    redisTemplate.opsForValue().set(redisKey, value, expire);
                }
            }
        }
        //如果不为空，说明从数据库中查询出来了，可以返回了
        if (entity != null) {
            return entity;
        }
        //如果不等于缓存的默认值，说明有真实的数据，转成对象返回
        if (!value.equals(DefaultRedisNullValue)) {
            entity = JSON.parseObject(value, clazz);
        }
        return entity;
    }



}
